package com.nageoffer.shortlink.project.service.impl;

import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.StrUtil;
import com.alibaba.fastjson.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.nageoffer.shortlink.common.config.GotoDomainWhiteListConfiguration;
import com.nageoffer.shortlink.common.constant.RedisConstant;
import com.nageoffer.shortlink.common.convention.enums.ValidDateTypeEnum;
import com.nageoffer.shortlink.common.convention.exception.ClientException;
import com.nageoffer.shortlink.common.convention.exception.ServiceException;
import com.nageoffer.shortlink.common.dto.project.biz.ShortLinkStatsRecordDTO;
import com.nageoffer.shortlink.common.dto.project.req.ShortLinkBatchCreateReqDTO;
import com.nageoffer.shortlink.common.dto.project.req.ShortLinkCreateReqDTO;
import com.nageoffer.shortlink.common.dto.project.req.ShortLinkUpdateReqDTO;
import com.nageoffer.shortlink.common.dto.project.resp.*;
import com.nageoffer.shortlink.common.utils.HashUtil;
import com.nageoffer.shortlink.project.dao.entity.ShortLink;
import com.nageoffer.shortlink.project.dao.entity.ShortLinkGoto;
import com.nageoffer.shortlink.project.dao.mapper.ShortLinkGotoMapper;
import com.nageoffer.shortlink.project.dao.mapper.ShortLinkMapper;
import com.nageoffer.shortlink.project.dto.req.ShortLinkPageReqDTO;
import com.nageoffer.shortlink.project.mq.producer.RedisMQShortLinkStatsProducer;
import com.nageoffer.shortlink.project.service.ShortLinkService;
import com.nageoffer.shortlink.project.utils.LinkUtil;
import jakarta.servlet.ServletRequest;
import jakarta.servlet.ServletResponse;
import jakarta.servlet.http.Cookie;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RBloomFilter;
import org.redisson.api.RLock;
import org.redisson.api.RReadWriteLock;
import org.redisson.api.RedissonClient;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;

import java.io.IOException;
import java.util.*;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.atomic.AtomicReference;

@RequiredArgsConstructor
@Slf4j
@Service
public class ShortLinkServiceImpl extends ServiceImpl<ShortLinkMapper, ShortLink> implements ShortLinkService {
    private final RBloomFilter<String> shortUriCreateBloomFilter;
    private final ShortLinkGotoMapper shortLinkGotoMapper;
    private final RedissonClient redissonClient;
    private final StringRedisTemplate stringRedisTemplate;
    private final GotoDomainWhiteListConfiguration gotoDomainWhiteListConfiguration;
    private final RedisMQShortLinkStatsProducer redisMQShortLinkStatsProducer;
    @Value("${short-link.domain}")
    private String DOMAIN;

    @Override
    public ShortLinkCreateRespDTO createShortLink(ShortLinkCreateReqDTO reqParam) throws IOException {
        verificationWhitelist(reqParam.getOriginUrl());
        String linkSuffix = generateSuffix(reqParam);
        String fullShortUrl = DOMAIN+'/'+linkSuffix;
        ShortLink shortLink = BeanUtil.toBean(reqParam,ShortLink.class);
        shortLink.setShortUri(linkSuffix);
        shortLink.setFullShortUrl(fullShortUrl);
        shortLink.setTotalUv(0L);
        shortLink.setTotalPv(0L);
        shortLink.setTotalUip(0L);
        shortLink.setFavicon(LinkUtil.getFaviconUrl(reqParam.getOriginUrl()));
        shortLink.setEnableStatus(0);
        ShortLinkGoto shortLinkGoto = ShortLinkGoto.builder()
                .fullShortUrl(fullShortUrl)
                .gid(shortLink.getGid())
                .build();
        try {
            save(shortLink);
            shortLinkGotoMapper.insert(shortLinkGoto);
        } catch (DuplicateKeyException e) {
            log.warn("短链接：{}重复入库",fullShortUrl);
            shortUriCreateBloomFilter.add(fullShortUrl);
            throw new ServiceException("短链接重复生成");
        }
        stringRedisTemplate.opsForValue().set(
                RedisConstant.GOTO_SHORT_LINK_KEY+fullShortUrl,
                shortLink.getOriginUrl(),
                LinkUtil.linkValidDateConvert(shortLink.getValidDate()),
                TimeUnit.MICROSECONDS);
        shortUriCreateBloomFilter.add(fullShortUrl);
        return ShortLinkCreateRespDTO.builder()
                .gid(shortLink.getGid())
                .originUrl(shortLink.getOriginUrl())
                .fullShortUri("http://"+shortLink.getFullShortUrl())
                .build();
    }
    @Override
    public void updateShortLink(ShortLinkUpdateReqDTO reqParam){
        LambdaQueryWrapper<ShortLink> queryWrapper = Wrappers.lambdaQuery(ShortLink.class)
                .eq(ShortLink::getGid, reqParam.getOriginalGid())
                .eq(ShortLink::getFullShortUrl, reqParam.getFullShortUrl())
                .eq(ShortLink::getEnableStatus,0)
                .eq(ShortLink::getDelFlag, 0);
        ShortLink hasShortLink = baseMapper.selectOne(queryWrapper);
        if (hasShortLink==null) {
            throw new ClientException("短链接记录不存在");
        }
        ShortLink updateLink = ShortLink.builder()
                .shortUri(hasShortLink.getShortUri())
                .fullShortUrl(hasShortLink.getFullShortUrl())
                .totalPv(hasShortLink.getTotalPv())
                .totalUv(hasShortLink.getTotalUv())
                .totalUip(hasShortLink.getTotalUip())
                .favicon(hasShortLink.getFavicon())
                .createType(hasShortLink.getCreateType())
                .enableStatus(hasShortLink.getEnableStatus())
                .gid(reqParam.getGid())
                .originUrl(reqParam.getOriginUrl())
                .describe(reqParam.getDescribe())
                .validDateType(reqParam.getValidDateType())
                .validDate(reqParam.getValidDate())
                .build();
        if (Objects.equals(reqParam.getOriginalGid(),reqParam.getGid())) {
            LambdaUpdateWrapper<ShortLink> updateWrapper = Wrappers.lambdaUpdate(ShortLink.class)
                    .eq(ShortLink::getFullShortUrl, reqParam.getFullShortUrl())
                    .eq(ShortLink::getGid, reqParam.getGid())
                    .eq(ShortLink::getEnableStatus, 0)
                    .eq(ShortLink::getDelFlag, 0)
                    .set(Objects.equals(reqParam.getValidDateType(), ValidDateTypeEnum.PERMANENT.getType()),ShortLink::getValidDate,null);
            baseMapper.update(updateLink,updateWrapper);
        }else {
            //
            RReadWriteLock readWriteLock = redissonClient.getReadWriteLock(RedisConstant.LOCK_GID_UPDATE_KEY + reqParam.getFullShortUrl());
            RLock lock = readWriteLock.writeLock();
            if (!lock.tryLock()) {
                throw new ServiceException("短链接正在被访问，请稍后重试...");
            }
            try {
                LambdaUpdateWrapper<ShortLink> updateWrapper = Wrappers.lambdaUpdate(ShortLink.class)
                        .eq(ShortLink::getFullShortUrl, reqParam.getFullShortUrl())
                        .eq(ShortLink::getGid, reqParam.getOriginalGid())
                        .eq(ShortLink::getEnableStatus, 0)
                        .eq(ShortLink::getDelFlag, 0)
                        .set(ShortLink::getDelFlag,1)
                        .set(ShortLink::getDel_time,new Date());
                baseMapper.update(null,updateWrapper);
                baseMapper.insert(updateLink);
                LambdaUpdateWrapper<ShortLinkGoto> gotoWrapper = Wrappers.lambdaUpdate(ShortLinkGoto.class)
                        .eq(ShortLinkGoto::getFullShortUrl, reqParam.getFullShortUrl());
                shortLinkGotoMapper.delete(gotoWrapper);
                ShortLinkGoto shortLinkGoto = ShortLinkGoto.builder()
                        .fullShortUrl(reqParam.getFullShortUrl())
                        .gid(reqParam.getGid())
                        .build();
                Thread.sleep(10000);
                shortLinkGotoMapper.insert(shortLinkGoto);
            } catch (InterruptedException e) {
                throw new RuntimeException(e);
            } finally {
                lock.unlock();
            }
        }
        if (!Objects.equals(reqParam.getValidDate(),null)) {
            stringRedisTemplate.delete(RedisConstant.GOTO_SHORT_LINK_KEY+reqParam.getFullShortUrl());
            if (reqParam.getValidDate().after(new Date())) {
                stringRedisTemplate.delete(RedisConstant.NULL_SHORT_LINK_KEY+reqParam.getFullShortUrl());
            }
        }
    }

    @Override
    public ShortLinkBatchCreateRespDTO createBatchShortLink(ShortLinkBatchCreateReqDTO reqParam) throws IOException {
        List<String> originUrls = reqParam.getOriginUrls();
        List<String> describes = reqParam.getDescribes();
        ArrayList<ShortLinkBaseInfoRespDTO> result = new ArrayList<>();
        for (int i = 0; i < originUrls.size(); i++) {
            ShortLinkCreateReqDTO shortLinkCreateReqDTO = BeanUtil.toBean(reqParam, ShortLinkCreateReqDTO.class);
            shortLinkCreateReqDTO.setOriginUrl(originUrls.get(i));
            shortLinkCreateReqDTO.setDescribe(describes.get(i));
            try{
                ShortLinkCreateRespDTO respDTO = createShortLink(shortLinkCreateReqDTO);
                ShortLinkBaseInfoRespDTO baseInfoRespDTO = ShortLinkBaseInfoRespDTO.builder()
                        .fullShortUrl(respDTO.getFullShortUri())
                        .originUrl(respDTO.getOriginUrl())
                        .describe(describes.get(i))
                        .build();
                result.add(baseInfoRespDTO);
            }catch (Exception e){
                log.error("批量创建短链接失败，原始参数：{}",originUrls.get(i));
            }
        }
        return ShortLinkBatchCreateRespDTO.builder()
                .total(result.size())
                .baseLinkInfos(result)
                .build();
    }

    @Override
    public Page<ShortLinkPageRespDTO> getShortLinkPage(ShortLinkPageReqDTO reqParam) {
        Page<ShortLinkPageRespDTO> page = baseMapper.getShortLinkPage(reqParam);
        return page;
    }

    @Override
    public List<ShortLinkGroupCountRespDTO> groupShortLinkCount(List<String> reqParam) {
        QueryWrapper<ShortLink> queryWrapper = Wrappers.query(new ShortLink())
                .select("gid,count(*) as shortLinkCount")
                .eq("enable_status", 0)
                .eq("del_flag", 0)
                .in("gid", reqParam)
                .groupBy("gid");
        List<Map<String, Object>> maps = baseMapper.selectMaps(queryWrapper);
        return BeanUtil.copyToList(maps, ShortLinkGroupCountRespDTO.class);
    }

    @Override
    public void shortLinkRedirect(String shortUri, HttpServletRequest request, HttpServletResponse response) throws IOException {
        String fullShortUrl = DOMAIN + "/" + shortUri;
        String originUrl = stringRedisTemplate.opsForValue().get(RedisConstant.GOTO_SHORT_LINK_KEY + fullShortUrl);
        if (StrUtil.isNotBlank(originUrl)){
            ShortLinkStatsRecordDTO statsRecordDTO = buildLinkStatsRecordAndSetUser(fullShortUrl, request, response);
            shortLinkStats(fullShortUrl,null,statsRecordDTO);
            response.sendRedirect(originUrl);
            return;
        }
        //布隆过滤器防缓存穿透（恶意请求不存在的短链接）
        boolean contains = shortUriCreateBloomFilter.contains(fullShortUrl);
        if (!contains) {
            response.sendRedirect("/page/notfound");
            return;
        }
        //存空值防缓存穿透（恶意请求不存在的短链接）
        String s = stringRedisTemplate.opsForValue().get(RedisConstant.NULL_SHORT_LINK_KEY + fullShortUrl);
        if (StrUtil.isNotBlank(s)) {
            response.sendRedirect("/page/notfound");
            return;
        }
        //为什么要上锁？某个短链接突然失效，导致大量请求同时访问数据库（应对缓存击穿问题）
        RLock lock = redissonClient.getLock(RedisConstant.LOCK_GOTO_SHORT_LINK_KEY + fullShortUrl);
        lock.lock();
        try {
            //双重判定锁：
            originUrl = stringRedisTemplate.opsForValue().get(RedisConstant.GOTO_SHORT_LINK_KEY + fullShortUrl);
            if (StrUtil.isNotBlank(originUrl)){
                response.sendRedirect(originUrl);
                return;
            }
            //短链接路由表查询短链接所在组id
            LambdaQueryWrapper<ShortLinkGoto> linkGotoWrapper = Wrappers.lambdaQuery(ShortLinkGoto.class)
                    .eq(ShortLinkGoto::getFullShortUrl, fullShortUrl);
            ShortLinkGoto shortLinkGoto = shortLinkGotoMapper.selectOne(linkGotoWrapper);
            if (shortLinkGoto==null) {
                stringRedisTemplate.opsForValue().set(RedisConstant.NULL_SHORT_LINK_KEY+fullShortUrl,".",30000, TimeUnit.MILLISECONDS);
                //风控
                return;
            }
            //从数据库查询短链接
            LambdaQueryWrapper<ShortLink> shortLinkWrapper = Wrappers.lambdaQuery(ShortLink.class)
                    .eq(ShortLink::getGid, shortLinkGoto.getGid())
                    .eq(ShortLink::getFullShortUrl, fullShortUrl)
                    .eq(ShortLink::getDelFlag, 0)
                    .eq(ShortLink::getEnableStatus, 0);
            ShortLink shortLink = baseMapper.selectOne(shortLinkWrapper);
            if (shortLink==null || (shortLink.getValidDate()!=null && shortLink.getValidDate().before(new Date()))) {
                //有效期不为空，则有效时间要在当前之后
                stringRedisTemplate.opsForValue().set(RedisConstant.NULL_SHORT_LINK_KEY+fullShortUrl,".",30000, TimeUnit.MILLISECONDS);
                response.sendRedirect("/page/notfound");
                return;
            }
            //缓存预热
            stringRedisTemplate.opsForValue().set(
                    RedisConstant.GOTO_SHORT_LINK_KEY+fullShortUrl,
                    shortLink.getOriginUrl(),
                    LinkUtil.linkValidDateConvert(shortLink.getValidDate()),
                    TimeUnit.MICROSECONDS);
            ShortLinkStatsRecordDTO statsRecordDTO = buildLinkStatsRecordAndSetUser(fullShortUrl, request, response);
            shortLinkStats(fullShortUrl,shortLink.getGid(),statsRecordDTO);
            //重定向
            response.sendRedirect(shortLink.getOriginUrl());
        }finally {
            lock.unlock();
        }
    }
    private ShortLinkStatsRecordDTO buildLinkStatsRecordAndSetUser(String fullShortUrl, ServletRequest request, ServletResponse response) {
        AtomicBoolean uvFirstFlag = new AtomicBoolean();
        Cookie[] cookies = ((HttpServletRequest) request).getCookies();
        AtomicReference<String> uv = new AtomicReference<>();
        Runnable addResponseCookieTask = () -> {
            uv.set(UUID.randomUUID().toString());
            Cookie uvCookie = new Cookie("uv", uv.get());
            uvCookie.setMaxAge(60 * 60 * 24 * 30);
            uvCookie.setPath(StrUtil.sub(fullShortUrl, fullShortUrl.indexOf("/"), fullShortUrl.length()));
            ((HttpServletResponse) response).addCookie(uvCookie);
            uvFirstFlag.set(Boolean.TRUE);
            stringRedisTemplate.opsForSet().add("short-link:stats:uv:" + fullShortUrl, uv.get());
        };
        if (ArrayUtil.isNotEmpty(cookies)) {
            Arrays.stream(cookies)
                    .filter(each -> Objects.equals(each.getName(), "uv"))
                    .findFirst()
                    .map(Cookie::getValue)
                    .ifPresentOrElse(each -> {
                        uv.set(each);
                        Long uvAdded = stringRedisTemplate.opsForSet().add("short-link:stats:uv:" + fullShortUrl, each);
                        uvFirstFlag.set(uvAdded != null && uvAdded > 0L);
                    }, addResponseCookieTask);
        } else {
            addResponseCookieTask.run();
        }
        String remoteAddr = LinkUtil.getIp(((HttpServletRequest) request));
        String os = LinkUtil.getOs(((HttpServletRequest) request));
        String browser = LinkUtil.getBrowser(((HttpServletRequest) request));
        String device = LinkUtil.getDevice(((HttpServletRequest) request));
        String network = LinkUtil.getNetwork(((HttpServletRequest) request));
        Long uipAdded = stringRedisTemplate.opsForSet().add("short-link:stats:uip:" + fullShortUrl, remoteAddr);
        boolean uipFirstFlag = uipAdded != null && uipAdded > 0L;
        return ShortLinkStatsRecordDTO.builder()
                .fullShortUrl(fullShortUrl)
                .uv(uv.get())
                .uvFirstFlag(uvFirstFlag.get())
                .uipFirstFlag(uipFirstFlag)
                .remoteAddr(remoteAddr)
                .os(os)
                .browser(browser)
                .device(device)
                .network(network)
                .build();
    }
    @Override
    public void shortLinkStats(String fullShortUrl,String gid,ShortLinkStatsRecordDTO statsRecord){
        HashMap<String, String> producerMap = new HashMap<>();
        producerMap.put("fullShortUrl",fullShortUrl);
        producerMap.put("gid",gid);
        producerMap.put("statsRecord", JSON.toJSONString(statsRecord));
        redisMQShortLinkStatsProducer.send(producerMap);
    }
    private String generateSuffix(ShortLinkCreateReqDTO reqParam){
        int count =0;
        String shortUri;
        while (true){
            if (count>10){
                throw new ServiceException("短链接生成频繁，请稍后重试");
            }
            String originUrl = reqParam.getOriginUrl()+ UUID.randomUUID();
            shortUri= HashUtil.hashToBase62(originUrl);
            String fullShortUrl = DOMAIN+'/'+shortUri;
            if (!shortUriCreateBloomFilter.contains(fullShortUrl)) {
                break;
            }
            count++;
        }
        return shortUri;
    }

    private void verificationWhitelist(String originUrl) {
        Boolean enable = gotoDomainWhiteListConfiguration.getEnable();
        if (enable == null || !enable) {
            return;
        }
        String domain = LinkUtil.extractDomain(originUrl);
        if (StrUtil.isBlank(domain)) {
            throw new ClientException("跳转链接填写错误");
        }
        List<String> details = gotoDomainWhiteListConfiguration.getDetails();
        if (!details.contains(domain)) {
            throw new ClientException("演示环境为避免恶意攻击，请生成以下网站跳转链接：" + gotoDomainWhiteListConfiguration.getNames());
        }
    }

}
